#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<assert.h>
#include<unistd.h>
#include<ctype.h>
#include<errno.h>

#define NUM 1024
#define OPT_NUM 64 //选项数

//定义命令是哪种类型的重定向 0-没有重定向类型 1-输入重定向类型 2-输出重定向 3-追加重定向
#define NONE_REDIR   0
#define INPUT_REDIR  1
#define OUTPUT_REDIR 2
#define APPEND_REDIR  3
//用于跳过空格的宏函数,一般来说,定义宏函数时用do{....}while(0)的结构来定义,函数代码放入{}中,且while(0)调用时该代码块只执行一次, 且while(0)后面没有";",在后续使用宏函数时,后面带";"都不会出错,没有影响-->写宏函数的小技巧
#define trimSpace(start) do{\
    while(isspace(*start)) ++start;\
}while(0)


char lineCommand[NUM]; //命令行字符数组
char* myargv[OPT_NUM]; //指针数组
int lastCode = 0;
int lastSig = 0;

int redirType = NONE_REDIR;//标识符用于标定该命令是哪种类型的标识符,默认为没有重定向类型
char* redirFile = NULL;//如果有重定向,那么该变量存储重定向的文件名;如果没有,默认为NULL

//"ls -a -l -i > myfile.txt" --> "ls -a -l -i" (>) "myfile.txt" 
void commandCheak(char* commands)
{
    assert(commands);    
    char* start = commands;//定义字符串的头指针
    char* end = commands + strlen(commands);//定义字符串的尾指针
    
    while(start<end)
    {
        if(*start == '>')
        {
            *start = '\0';
            start++;
            if(*start == '>')//追加重定向
            {
                //"ls -a >> myfile.txt"
                //填写重定向信息
                redirType = APPEND_REDIR;//重定向类型
                start++;
            }
            else//输出重定向
            {
                //"ls -a >    myfile.txt"
                //填写重定向信息
                redirType = OUTPUT_REDIR;//重定向类型
            }
            //填写重定向信息
            trimSpace(start);//跳过空格
            redirFile  = start;//重定向文件
            break;
        }
        else if(*start == '<')//输入重定向
        {
            //"cat < myfile.txt"
            *start = '\0';
            start++;
            trimSpace(start);//跳过空格
            //填写重定向信息
            redirType = INPUT_REDIR;//重定向类型
            redirFile = start;//重定向文件
            break;
        }
        else 
        {
            start++;
        }
    }

}


int main()
{
    while(1)
    {
        //初始化,防止重定向后对全局变量的改变对下一个指令有影响
        redirType = NONE_REDIR;
        redirFile = NULL;
        errno = 0;
        
        //输出提示符
        printf("用户名@主机名 当前路径#");
        fflush(stdout);//刷新缓冲区

        //获取用户输入，输入的时候,还会输入一个\n
        char* s = fgets(lineCommand,sizeof(lineCommand)-1,stdin);//按行获取字符串
        assert(s != NULL);
        (void)s;
        //输入时字符数组最后会有一个\n,要处理清除最后的\n
        lineCommand[strlen(lineCommand)-1] = 0;
        //printf("test:%s\n",lineCommand);
        
        //普通命令的字符串把字符串按空格拆分即可
        //"ls -a -l -i" --> "ls" "-a" "-l" "-i"
        //重定向类型的字符串要把字符串按重定向符前后拆分成两个字符串--> 把冲定向符号改成"\0",把一个字符串拆成两个字符串使用 
        //"ls -a -l -i > myfile.txt" --> "ls -a -l -i" (>) "myfile.txt" 
        //"ls -a -l -i >> myfile.txt" --> "ls -a -l -i" (>>) "myfile.txt" 
        //"cat < myfile.txt" --> "cat" (<) "myfile.txt"
        commandCheak(lineCommand);//对输入进来的lineCommand做检查,判断是哪种类型命令,并处理相应的的类型的重定向文件
        //调用完commandCheak()后,无论有没有重定向,那么对于命令内容已经用'\0'做分割
        

        //切割字符串,放入命令行参数数组中
        myargv[0] = strtok(lineCommand," ");
        int i = 1;
        
        //处理ls指令默认带颜色
        if(myargv[0] != NULL && strcmp(myargv[0] , "ls") == 0)
        {
            myargv[i++] = (char*)"--color=auto";
        }
        //如果没有字串可以切割，strtok返回NULL,myargv[end] = NULL(命令行参数数组的最后一个也是NULL)
        while(myargv[i++] = strtok(NULL," "));
        
        //处理cd指令,如果是cd指令那么不需要创建子进程，让shell自己执行对应的命令，本质就是执行系统接口
        //像这种不需要创建子进程来执行，而是让shell自己执行的命令--叫做内建/内置命令
        //echo能查shell本地环境变量的原因是echo是一个内建/内置命令
        if(myargv[0] != NULL && strcmp(myargv[0],"cd") == 0)
        {
            if(myargv[1] != NULL) chdir(myargv[1]);
            continue;
        }

        //内建命令-->echo
        //echo指令
        if(myargv[0] != NULL && myargv[1] != NULL && strcmp(myargv[0],"echo") == 0)
        {
            if(strcmp(myargv[1],"$?") == 0)
            {
                printf("%d,%d\n",lastCode,lastSig);
            }
            else
            {
                printf("%s\n",myargv[1]);
            }
            continue;
        }

        //测试是否成功
#ifdef DEBUG 
        for(int i = 0; myargv[i] ; i++)
        {
            printf("myargv[%d]:%s\n",i,myargv[i]);
        }
#endif
        
        //执行命令
        pid_t id = fork();
        assert(id != -1);//防止子进程没有建立
        
        if(id == 0)//子进程去调用输入的命令
        {
            //因为命令是子进程进行的,所以真正的重定向工作一定是由子进程来完成
            //如何重定向,是父进程要给子进程提供信息的
            //这里重定向会影响父进程吗?
            //-->不会让子进程影响父进程,因为进程具有独立性
            //在创建子进程时,不仅会为子进程创建自己的PCB,虚拟内存,页表等,还会创建文件描述符表来确保进程间的独立性
            //注意:虽然会为子进程创建对应的文件描述符表(文件描述符表属于进程管理),但不会为子进程再创建一份打开的文供子进程单独使用,打开的文件是属于文件系统,如果子进程或父进程要关闭对应文件,只需要从自己的文件描述符表中去除即可
            switch(redirType)
            {
                case NONE_REDIR:
                    //什么都不做
                    break;
                case INPUT_REDIR:
                    {
                        int fd = open(redirFile , O_RDONLY);
                        if(fd < 0)
                        {
                            perror("open:");
                            exit(errno);
                        }
                        //重定向的文件已经成功打开
                        dup2(fd , 0);//重定向输入
                    }
                    break;
                case OUTPUT_REDIR:
                case APPEND_REDIR:
                    {
                        umask(0);
                        int flags = O_WRONLY | O_CREAT;
                        if(redirType == OUTPUT_REDIR)
                            flags |= O_TRUNC; 
                        else if(redirType == APPEND_REDIR)
                            flags |= O_APPEND; 
                        int fd = open(redirFile , flags , 0666);
                        if(fd < 0)
                        {
                            perror("open:");
                            exit(errno);
                        }
                        //重定向的文件已经成功打开
                        dup2(fd , 1);//重定向输出/追加
                    }
                    break;
                default:
                    printf("BUG?\n");
                    break;
            }
            
            //执行程序替换的时候,会不会影响曾经进程打开的重定向的文件?
            //-->不会,因为文件描述符表是一种内核数据结构,当你发生程序替换时,替换的是代码和数据,并不会影响内核数据结构的数据,并不影响PCB及其PCB内部的各种PID(进程)细节
            execvp(myargv[0],myargv);//v:传递如何执行调用程序的参数放到数组传递，l:参数一个一个传递
                                     //p:传递调用程序的名称,会自动去PATH中查找
            exit(1);
        }
        
        //父进程用于等待子进程
        int status = 0;
        pid_t ret = waitpid(id,&status,0);
        assert(ret > 0);
        (void)ret;
        lastCode = ((status>>8)&0xFF);
        lastSig = (status&0x7f);

    }


    return 0;
}
